package maze.gui;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.image.BufferedImage;
import java.util.EnumSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;

import javax.swing.JComponent;

import maze.model.CellSizeModel;
import maze.model.Direction;
import maze.model.MazeCell;
import maze.model.MazeModel;
import maze.model.RobotPathModel;
import maze.util.Listener;

/**
 * This swing component displays a graphical view of a maze. It also has the
 * capability to draw different things onto the maze like an image for the
 * robots location.<br />
 * Painting is handled by a UI delegate {@link maze.gui.MazePainter} that allows
 * different themes to be used.
 * @author Luke Last
 */
public class MazeView extends JComponent implements Listener<MazeCell>
{
   private static final boolean PRINT_DEBUG = false;
   private static final long serialVersionUID = 3249468255178771818L;
   private static final int WALL_SIZE_DIVIDER = 6;
   private static final int MAX_CELLS_TO_DRAW = 64;
   /**
    * Determines the size of the walls relative to the cell size.
    */
   protected int wallSizeDivider = WALL_SIZE_DIVIDER;
   /**
    * The maze model that stores the configuration of the maze.
    */
   protected MazeModel model;
   /**
    * The background image holds the rendered maze cells. When cells are
    * invalidated they are redrawn on this background image. When the screen is
    * resized that reference is set to null so a newly sized image can be
    * created.
    */
   private BufferedImage backgroundImage;
   /**
    * Stores a reference to the graphics object for the background image. This
    * prevents is from having to request it from the image every time.
    */
   private Graphics2D backgroundGraphics;
   /**
    * Stores the sizes of a cell and its walls.
    */
   protected final CellSizeModel csm = new CellSizeModel(false);
   /**
    * UI delegate used for drawing each maze component.
    */
   protected MazePainter painter = new MazePainterDefault();
   /**
    * The current location of the robot while it is animating.
    */
   private volatile Point robotLocation = null;
   /**
    * The current rotation of the robot in radians.
    */
   private volatile double robotRotation = 0.0;
   /**
    * This is null unless an animation is running and then the
    * <code>RobotAnimator</code> will populate it. This stores information about
    * the robots paths and history.
    */
   private RobotPathModel robotPathModel;
   /**
    * A flag for redrawing everything. When set true this tells us we are
    * redrawing the whole view which means we must draw the outside walls.
    * @see MazeView#invalidateAllCells()
    * @see MazeView#paintComponent(Graphics)
    */
   private boolean repaintAll = true;
   /**
    * Stores a set of maze cells that have been invalidated and need to be
    * redrawn. ALL access to this set should be synchronized on the object
    * itself.
    */
   private final Set<MazeCell> invalidatedCells = new TreeSet<MazeCell>();

   private int[][] understandingInt = null;
   private Direction[][] understandingDir = null;

   private boolean drawPathCurrent = true;
   private boolean drawPathFirst = true;
   private boolean drawPathBest = true;
   private boolean drawUnderstanding = true;
   private boolean drawFog = true;

   /**
    * Constructor.
    */
   public MazeView()
   {
      // We maintain our own background image buffer so we can turn this off.
      this.setDoubleBuffered(false);
      // For catching resize events.
      this.addComponentListener(new ComponentAdapter()
      {
         /**
          * When the component is resized we have to resize the cells and walls
          * and resize the background image.
          */
         @Override
         public void componentResized(ComponentEvent e)
         {
            updateViewSize();
         }
      });
   }

   /**
    * Draws an arrow graphic.
    * @param g What do draw on.
    * @param local Direction to point the arrow.
    * @param x Horizontal pixel location to draw arrow.
    * @param y Vertical pixel location to draw arrow.
    */
   private void drawArrow(final Graphics2D g, final Direction local, final int x, final int y)
   {
      //Draws an arrow in the direction of "local" centered on the point (x,y)
      if (local.equals(Direction.North))
      {
         final int[] ys =
         {
            y + this.csm.getCellHeight() * 3 / 8, y, y, y - this.csm.getCellHeight() * 3 / 8, y, y
         };
         final int[] xs =
         {
            x, x - this.csm.getCellWidth() / 8, x - this.csm.getCellWidth() / 4, x,
            x + this.csm.getCellWidth() / 4, x + this.csm.getCellWidth() / 8
         };
         g.drawPolygon(xs, ys, 6);
      }
      if (local.equals(Direction.South))
      {
         final int[] ys =
         {
            y - this.csm.getCellHeight() * 3 / 8, y, y, y + this.csm.getCellHeight() * 3 / 8, y, y
         };
         final int[] xs =
         {
            x, x - this.csm.getCellWidth() / 8, x - this.csm.getCellWidth() / 4, x,
            x + this.csm.getCellWidth() / 4, x + this.csm.getCellWidth() / 8
         };
         g.drawPolygon(xs, ys, 6);
      }
      if (local.equals(Direction.West))
      {
         final int[] xs =
         {
            x + this.csm.getCellWidth() * 3 / 8, x, x, x - this.csm.getCellWidth() * 3 / 8, x, x
         };
         final int[] ys =
         {
            y, y - this.csm.getCellHeight() / 8, y - this.csm.getCellHeight() / 4, y,
            y + this.csm.getCellHeight() / 4, y + this.csm.getCellHeight() / 8
         };
         g.drawPolygon(xs, ys, 6);
      }
      if (local.equals(Direction.East))
      {
         final int[] xs =
         {
            x - this.csm.getCellWidth() * 3 / 8, x, x, x + this.csm.getCellWidth() * 3 / 8, x, x
         };
         final int[] ys =
         {
            y, y - this.csm.getCellHeight() / 8, y - this.csm.getCellHeight() / 4, y,
            y + this.csm.getCellHeight() / 4, y + this.csm.getCellHeight() / 8
         };
         g.drawPolygon(xs, ys, 6);
      }
   }

   /**
    * The primary draw method for a cell. This should draw all aspects of a
    * cell.
    * @param g Where to draw.
    * @param cell The cell in question.
    */
   protected void drawCell(final Graphics2D g, final MazeCell cell)
   {
      if (PRINT_DEBUG)
         System.out.println(System.currentTimeMillis() + " Drawing Cell: " + cell);
      this.painter.drawCellBackground(g, this.getCellAreaInner(cell));

      if (this.model.getWall(cell, Direction.East).isSet())
      {
         this.painter.drawWallSet(g, this.getWallArea(cell, Direction.East));
      }
      else
      {
         this.painter.drawWallEmpty(g, this.getWallArea(cell, Direction.East));
      }
      if (this.model.getWall(cell, Direction.South).isSet())
      {
         this.painter.drawWallSet(g, this.getWallArea(cell, Direction.South));
      }
      else
      {
         this.painter.drawWallEmpty(g, this.getWallArea(cell, Direction.South));
      }
      this.painter.drawPeg(g, this.getPegArea(cell));
      if (this.robotPathModel != null)
      {
         //Draw the fog of war.
         if (this.drawFog && !this.robotPathModel.hasCellBeenVisited(cell))
         {
            final Rectangle area = this.getCellArea(cell);
            final MazeCell east = cell.neighbor(Direction.East);
            final MazeCell south = cell.neighbor(Direction.South);
            if (east.isInRange(this.model.getSize()) &&
                this.robotPathModel.hasCellBeenVisited(east))
            {
               area.width -= this.csm.getWallWidth();
            }
            if (south.isInRange(this.model.getSize()) &&
                this.robotPathModel.hasCellBeenVisited(south))
            {
               area.height -= this.csm.getWallHeight();
            }
            this.painter.drawFog(g, area);
         }
         // Draw a current path of dots.
         /*
         if (this.drawPathCurrent && this.robotPathModel.getPathRecent().contains(cell))
         {
            final EnumSet<Direction> directions = EnumSet.noneOf(Direction.class);
            for (final Direction dir : this.getAdjacentDirections(cell))
            {
               if (!this.model.getWall(cell, dir).isSet() &&
                   this.robotPathModel.getPathRecent().contains(cell.neighbor(dir)))
               {
                  directions.add(dir);
               }
            }
            this.painter.drawRunCurrent(g, this.getCellAreaInner(cell), directions);
         }
         */
      }
   }

   /**
    * Draws cells that have been invalidated. We only draw a limited number of
    * cells per call and if not all invalidated cells were drawn repaint() is
    * called.
    * @param g Where to draw.
    */
   private void drawInvalidatedCells(final Graphics2D g)
   {
      final boolean notDone;
      synchronized (this.invalidatedCells)
      {
         final Iterator<MazeCell> itr = this.invalidatedCells.iterator();
         int limit = MAX_CELLS_TO_DRAW;
         while (itr.hasNext() && 0 < limit--)
         {
            this.drawCell(g, itr.next());
            itr.remove();
         }
         notDone = this.invalidatedCells.isEmpty() ? false : true;
      }
      if (notDone)
         this.repaint();
   }

   /**
    * Draws the top and left outside walls as these don't fall inside of any
    * cells.
    * @param g Where to draw.
    */
   private void drawOutsideWalls(final Graphics2D g)
   {
      if (this.model != null && this.painter != null)
      {
         final Rectangle pegArea = new Rectangle(0,
                                                 0,
                                                 this.csm.getWallWidth(),
                                                 this.csm.getWallHeight());
         final Rectangle wallArea = new Rectangle(this.csm.getWallWidth(),
                                                  0,
                                                  this.csm.getCellWidthInner(),
                                                  this.csm.getWallHeight());

         for (int i = 0; i <= this.model.getSize().width; i++)
         {
            this.painter.drawPeg(g, pegArea);
            pegArea.x += this.csm.getCellWidth();
            // We draw more pegs than walls.
            if (i == 0)
            {
               continue;
            }
            this.painter.drawWallSet(g, wallArea);
            wallArea.x += this.csm.getCellWidth();
         }

         pegArea.setLocation(0, this.csm.getCellHeight());
         wallArea.setSize(this.csm.getWallWidth(), this.csm.getCellHeightInner());
         wallArea.setLocation(0, this.csm.getWallHeight());
         // Draw the left side column of pegs and walls.
         for (int i = 1; i <= this.model.getSize().height; i++)
         {
            this.painter.drawPeg(g, pegArea);
            pegArea.y += this.csm.getCellHeight();
            this.painter.drawWallSet(g, wallArea);
            wallArea.y += this.csm.getCellHeight();
         }
      }
   }

   /**
    * Draw a robot path onto the maze. The path will have the width of the cell
    * walls.
    * @param g Where to draw.
    * @param path The cell to cell path to draw.
    * @param offset Shift the path by the given amount so that you can draw
    *           multiple paths without them overlapping. A value of 0 draws to
    *           the center of the cell.
    * @param trimTail If set to true the tail of the path will be trimmed to the
    *           location of the robot. This allows the current path to finish
    *           right on the current robot position.
    */
   private void drawPath(final Graphics2D g, final List<MazeCell> path, final int offset,
         boolean trimTail)
   {
      if (path != null && !path.isEmpty())
      {
         MazeCell here = path.get(0);
         MazeCell there;
         int x, y;
         int width, height;
         for (int i = 1; i < path.size(); i++)
         {
            there = path.get(i);
            // Check for a rare case where a lack of thread safety changes the list while in use.
            if (there == null)
               return;
            final Point center = this.getCellCenterInner(here);
            if (here.getX() < there.getX())
            {
               //here is west of there, going east.
               x = center.x + this.csm.getWallWidthHalf();
               y = center.y - this.csm.getWallHeightHalf();
               width = this.csm.getCellWidth();
               height = this.csm.getWallHeight();
            }
            else if (here.getX() > there.getX())
            {
               //here is east of there, going west.
               x = center.x - this.csm.getWallWidthHalf() - this.csm.getCellWidth();
               y = center.y - this.csm.getWallHeightHalf();
               width = this.csm.getCellWidth();
               height = this.csm.getWallHeight();
            }
            else if (here.getY() > there.getY())
            {
               //here is south of there, going north.
               x = center.x - this.csm.getWallWidthHalf();
               y = center.y - this.csm.getWallHeightHalf() - this.csm.getCellHeight();
               width = this.csm.getWallWidth();
               height = this.csm.getCellHeight();
            }
            else
            {
               //here is north of there, going south.
               x = center.x - this.csm.getWallWidthHalf();
               y = center.y + this.csm.getWallHeightHalf();
               width = this.csm.getWallWidth();
               height = this.csm.getCellHeight();
            }
            // If we are at the last cell and we are trimming the tail up to the robot.
            if (i == path.size() - 1 && trimTail && this.robotLocation != null)
            {
               if (here.getX() < there.getX())
               {
                  //here is west of there, going east.
                  width = Math.abs(x - this.robotLocation.x);
               }
               else if (here.getX() > there.getX())
               {
                  //here is east of there, going west.
                  width -= this.robotLocation.x - x;
                  x = this.robotLocation.x;

               }
               else if (here.getY() > there.getY())
               {
                  //here is south of there, going north.
                  height -= this.robotLocation.y - y;
                  y = this.robotLocation.y;
               }
               else
               {
                  //here is north of there, going south.
                  height = this.robotLocation.y - y;
               }
            }
            g.fillRect(x - offset, y + offset, width, height);
            here = there;
         }
      }
   }

   /**
    * Draws the very top of the maze view. This is the most frequently called
    * draw method and is called every time the view is repainted. Because of
    * this it should run as fast as possible. It is much better to draw in the
    * <code>drawCell()</code> method.
    * @param g What to draw on.
    */
   private void drawTopLayer(final Graphics2D g)
   {
      this.setRenderingQualityLow(g);
      final RobotPathModel pathModel = this.robotPathModel;
      if (pathModel != null)
      {
         if (this.drawPathFirst)
         {
            g.setPaint(this.painter.getRunFirst());
            this.drawPath(g, pathModel.getPathFirst(), this.csm.getWallWidth(), false);
         }
         if (this.drawPathBest)
         {
            g.setPaint(this.painter.getRunBest());
            this.drawPath(g, pathModel.getPathBest(), -this.csm.getWallWidth(), false);
         }
         if (this.drawPathCurrent)
         {
            g.setPaint(this.painter.getRunCurrent());
            this.drawPath(g, pathModel.getPathRecent(), 0, true);
         }
      }
      if (this.drawUnderstanding)
      {
         this.drawUnderstanding(g);
      }
      if (this.getRobotLocation() != null)
      {
         this.setRenderingQualityHigh(g);
         this.painter.drawRobot(g,
                                this.getRobotLocation(),
                                this.getRobotRotation(),
                                this.csm.getCellWidthInner(),
                                this.csm.getCellHeightInner());
      }
   }

   /**
    * Draws the arrows and numbers on the maze.
    * @param g What to draw on.
    */
   private void drawUnderstanding(final Graphics2D g)
   {
      MazeCell here;
      if (understandingInt != null)
      {
         int local;
         for (int i = 1; i <= model.getSize().width; i++)
         {
            for (int j = 1; j <= model.getSize().height; j++)
            {
               here = MazeCell.valueOf(i, j);
               g.setColor(Color.BLACK);
               final Point center = this.getCellCenterInner(here);
               local = understandingInt[i - 1][j - 1];
               g.drawString(String.valueOf(local), center.x - 6, center.y + 2);
            }
         }
      }
      else if (understandingDir != null)
      {
         Direction local;
         for (int i = 1; i <= model.getSize().width; i++)
         {
            for (int j = 1; j <= model.getSize().height; j++)
            {
               here = MazeCell.valueOf(i, j);
               final Point center = this.getCellCenterInner(here);
               g.setColor(Color.BLACK);
               local = understandingDir[i - 1][j - 1];
               if (local != null)
               {
                  drawArrow(g, local, center.x, center.y);
               }
            }
         }
      }
   }

   /**
    * This event is triggered to invalidate a cell that needs to be repainted.
    */
   @Override
   public void eventFired(final MazeCell cell)
   {
      this.invalidateCell(cell);
   }

   /**
    * Gets all the cells that are adjacent to the given one.
    * @param cell The cell in question.
    * @return All adjacent or connecting cells.
    */
   @SuppressWarnings("unused")
   private MazeCell[] getAdjacentCells(final MazeCell cell)
   {
      final Direction[] dirs = this.getAdjacentDirections(cell).toArray(new Direction[0]);
      final MazeCell[] result = new MazeCell[dirs.length];
      for (int i = 0; i < dirs.length; i++)
      {
         result[i] = cell.neighbor(dirs[i]);
      }
      return result;
   }

   /**
    * Gets a direction for each adjacent cell that exists. The only time one
    * doesn't exists is when the given cell is on the maze edge.
    * @param cell The cell in question.
    * @return A set of directions with each direction confirming the existence
    *         of a neighbor cell.
    */
   private EnumSet<Direction> getAdjacentDirections(final MazeCell cell)
   {
      // We start with all and then remove because it is less likely.
      final EnumSet<Direction> directions = EnumSet.allOf(Direction.class);
      if (cell.getX() == 1)
      {
         directions.remove(Direction.West);

      }
      if (cell.getY() == 1)
      {
         directions.remove(Direction.North);

      }
      if (cell.getX() == this.model.getSize().width)
      {
         directions.remove(Direction.East);

      }
      if (cell.getY() == this.model.getSize().height)
      {
         directions.remove(Direction.South);

      }
      return directions;
   }

   /**
    * Get the graphics object that draws onto the background image. All drawing
    * onto the background image must use this.
    * @return The graphics object.
    */
   private Graphics2D getBackgroundGraphics()
   {
      if (this.backgroundImage == null)
      {
         // If creating a new background image make sure we paint it.
         this.backgroundGraphics = null;
         this.backgroundImage = new BufferedImage(getWidth(),
                                                  getHeight(),
                                                  BufferedImage.TYPE_INT_ARGB);
      }
      if (this.backgroundGraphics == null)
      {
         this.backgroundGraphics = this.backgroundImage.createGraphics();
         this.setRenderingQualityHigh(this.backgroundGraphics);
      }
      return this.backgroundGraphics;
   }

   /**
    * Get the pixel space of a cell.
    * @param cell The cell in question.
    * @return The location and area in pixels of where the cell is located.
    */
   protected Rectangle getCellArea(final MazeCell cell)
   {
      return new Rectangle(this.csm.getWallWidth() + cell.getXZeroBased() * this.csm.getCellWidth(),
                           this.csm.getWallHeight() +
                                 cell.getYZeroBased() *
                                 this.csm.getCellHeight(),
                           this.csm.getCellWidth(),
                           this.csm.getCellHeight());
   }

   /**
    * Similar to <code>getCellArea</code> but just the inside of the cell
    * without the walls.
    * @param cell The cell in question.
    * @return Cell area minus the walls and corner peg.
    */
   protected Rectangle getCellAreaInner(final MazeCell cell)
   {
      return new Rectangle(this.csm.getWallWidth() + cell.getXZeroBased() * this.csm.getCellWidth(),
                           this.csm.getWallHeight() +
                                 cell.getYZeroBased() *
                                 this.csm.getCellHeight(),
                           this.csm.getCellWidthInner(),
                           this.csm.getCellHeightInner());
   }

   /**
    * Get the center of the inner cell part without the walls.
    * @param cell The cell in question.
    * @return The point in pixel coordinates.
    */
   protected Point getCellCenterInner(final MazeCell cell)
   {
      return new Point(this.csm.getWallWidth() +
                             (cell.getXZeroBased() * this.csm.getCellWidth()) +
                             (this.csm.getCellWidthInner() / 2),
                       this.csm.getWallHeight() +
                             (cell.getYZeroBased() * this.csm.getCellHeight()) +
                             (this.csm.getCellHeightInner() / 2));
   }

   /**
    * Get the size of the maze in pixels.
    */
   private Dimension getMazeSize()
   {
      if (this.model != null)
      {
         return new Dimension(this.model.getSize().width * this.csm.getCellWidth(),
                              this.model.getSize().height * this.csm.getCellHeight());
      }
      else
      {
         return new Dimension();
      }
   }

   /**
    * Get the maze model being used for this view.
    */
   public MazeModel getModel()
   {
      return model;
   }

   /**
    * Get the area of the peg of a cell.
    * @param cell The cell in question.
    * @return The location and size of the peg.
    */
   protected Rectangle getPegArea(final MazeCell cell)
   {
      return new Rectangle(cell.getX() * this.csm.getCellWidth(),
                           cell.getY() * this.csm.getCellHeight(),
                           this.csm.getWallWidth(),
                           this.csm.getWallHeight());
   }

   /**
    * Get the current location of the robot in absolute view coordinates.
    */
   public Point getRobotLocation()
   {
      return this.robotLocation;
   }

   /**
    * Get the current rotation of the robot in Radians.
    */
   public double getRobotRotation()
   {
      return this.robotRotation;
   }

   /**
    * Get the area of a cell wall.
    * @param cell The cell in question.
    * @param wall Which wall do you want. Must be East or South.
    * @return The absolute coordinates of the area.
    */
   protected Rectangle getWallArea(final MazeCell cell, final Direction wall)
   {
      switch (wall)
      {
         case East :
            return new Rectangle(cell.getX() * this.csm.getCellWidth(),
                                 this.csm.getWallHeight() +
                                       cell.getYZeroBased() *
                                       this.csm.getCellHeight(),
                                 this.csm.getWallWidth(),
                                 this.csm.getCellHeightInner());
         case South :
            return new Rectangle(this.csm.getWallWidth() +
                                       cell.getXZeroBased() *
                                       this.csm.getCellWidth(),
                                 cell.getY() * this.csm.getCellHeight(),
                                 this.csm.getCellWidthInner(),
                                 this.csm.getWallHeight());
         default :
            throw new IllegalArgumentException("Non supported direction: " + wall);
      }
   }

   /**
    * Invalidate and repaint all cells.
    */
   public void invalidateAllCells()
   {
      this.repaintAll = true;
      synchronized (this.invalidatedCells)
      {
         // We clear it first because it might have cells that are out of the current size.
         this.invalidatedCells.clear();
         for (int x = 1; x <= this.model.getSize().width; x++)
         {
            for (int y = 1; y <= this.model.getSize().height; y++)
            {
               this.invalidatedCells.add(MazeCell.valueOf(x, y));
            }
         }
      }
      this.repaint();
   }

   /**
    * Invalidate a cell and mark it to be redrawn.
    * @param cell The cell to be redrawn.
    */
   protected void invalidateCell(final MazeCell cell)
   {
      if (cell != null && this.model != null && cell.isInRange(this.model.getSize()))
      {
         if (PRINT_DEBUG)
            System.out.println(System.currentTimeMillis() + " Invalidating Cell: " + cell);
         synchronized (this.invalidatedCells)
         {
            this.invalidatedCells.add(cell);
         }
         super.repaint();
      }
   }

   public void loadUnderstanding(final int[][] understandingInt)
   {
      this.understandingInt = understandingInt;
   }

   public void loadUnderstandingDir(final Direction[][] arrows)
   {
      this.understandingDir = arrows;
   }

   /**
    * Master starting point for the custom painting. We want to draw as little
    * as we can to keep performance up.
    */
   @Override
   protected void paintComponent(final Graphics arg)
   {
      if (PRINT_DEBUG)
         System.out.println(System.currentTimeMillis() + " Painting Component");

      final Graphics2D bgg = this.getBackgroundGraphics();
      if (this.repaintAll)
      {
         this.repaintAll = false;
         this.drawOutsideWalls(bgg);
      }
      this.drawInvalidatedCells(bgg);
      final Graphics2D g = (Graphics2D) arg;
      g.drawImage(this.backgroundImage, null, 0, 0);
      this.drawTopLayer(g);
   }

   /**
    * @param drawFog the drawFog to set
    */
   public void setDrawFog(boolean drawFog)
   {
      if (this.drawFog != drawFog)
      {
         this.drawFog = drawFog;
         this.invalidateAllCells();
      }
   }

   /**
    * @param drawPathBest the drawPathBest to set
    */
   public void setDrawPathBest(final boolean drawPathBest)
   {
      this.drawPathBest = drawPathBest;
   }

   /**
    * @param drawPathCurrent the drawPathCurrent to set
    */
   public void setDrawPathCurrent(final boolean drawPathCurrent)
   {
      if (this.drawPathCurrent != drawPathCurrent)
      {
         this.drawPathCurrent = drawPathCurrent;
         this.invalidateAllCells();
      }
   }

   /**
    * @param drawPathFirst the drawPathFirst to set
    */
   public void setDrawPathFirst(final boolean drawPathFirst)
   {
      this.drawPathFirst = drawPathFirst;
   }

   /**
    * Set whether or not this view should draw/display understanding information
    * from the AI algorithm.
    * @param draw Setter value.
    */
   public void setDrawUnderstanding(final boolean draw)
   {
      if (this.drawUnderstanding != draw)
      {
         this.drawUnderstanding = draw;
         if (this.understandingInt != null || this.understandingDir != null)
         {
            this.invalidateAllCells();
         }
      }
   }

   /**
    * Set the maze model to use for this view.
    * @param model The new model to back this view.
    */
   public void setModel(final MazeModel model)
   {
      if (this.model != model)
      {
         if (this.model != null)
         {
            this.model.removeListener(this);
         }
         this.model = model;
         if (this.model != null)
         {
            this.model.addListener(this);
         }
         this.updateViewSize();
      }
   }

   /**
    * Set a new maze painting delegate for this maze view to use when drawing.
    * @param newPainterDelegate The new delegate to do drawing for this view.
    */
   public void setPainterDelegate(MazePainter newPainterDelegate)
   {
      if (newPainterDelegate != null && this.painter != newPainterDelegate)
      {
         this.painter = newPainterDelegate;
         // When changing the painter we want to redraw everything.
         this.updateViewSize();
      }
   }

   /**
    * Set the rendering hints to high quality.
    * @param g The graphics object to set the rendering hints on.
    */
   private void setRenderingQualityHigh(Graphics2D g)
   {
      g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
      g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
   }

   /**
    * Set the rendering hints to low quality.
    * @param g The graphics object to set the rendering hints on.
    */
   private void setRenderingQualityLow(Graphics2D g)
   {
      g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_SPEED);
      g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
   }

   /**
    * Set the robot path model that this view should use to draw information
    * about the robots path. This should be set while an animation is running
    * and then set to null after and when no animation is running.
    * @param model The model to set.
    */
   public void setRobotPathModel(final RobotPathModel model)
   {
      if (this.robotPathModel != model)
      {
         if (this.robotPathModel != null)
         {
            this.robotPathModel.removeListener(this);
         }
         this.robotPathModel = model;
         if (this.robotPathModel != null)
         {
            this.robotPathModel.addListener(this);
         }
         this.invalidateAllCells();
      }
   }

   /**
    * Sets a new position for the robot and then sets the view to repaint
    * itself.
    * @param newLocation The new location for the robot in absolute view
    *           coordinates.
    * @param newRotation The new rotation of the robot in Radians.
    */
   public void setRobotPosition(final Point newLocation, final double newRotation)
   {
      this.robotLocation = newLocation;
      this.robotRotation = newRotation;
      this.repaint();
   }

   /**
    * Recalculates the sizes of the cells and walls from the current size of the
    * component. We also delete the background image buffer so it can be
    * recreated at the new size. We also invalidate all cells so they can be
    * redrawn. This method itself returns quickly but it triggers some expensive
    * operations.
    */
   private void updateViewSize()
   {
      if (model != null)
      {
         this.backgroundImage = null; // Trigger creation of a new buffer image.
         csm.setCellWidth( (getWidth() - csm.getWallWidth()) / model.getSize().width);
         csm.setCellHeight( (getHeight() - csm.getWallHeight()) / model.getSize().height);
         final int wallSize = Math.min(csm.getCellWidth(), csm.getCellHeight()) /
                              this.wallSizeDivider;
         this.csm.setWallWidth(wallSize);
         this.csm.setWallHeight(wallSize);
         this.painter.setMazeSize(getMazeSize());
         // Have everything repainted.
         this.invalidateAllCells();
      }
   }
}